package org.knot.mockejb3.transcation;

import java.io.Serializable;
import java.lang.reflect.Method;

import javax.naming.*;
import javax.transaction.*;

import javax.ejb.*;

import org.apache.commons.logging.*;
import org.knot.mockejb3.MockContainer;
import org.knot.mockejb3.MockEjbSystemException;
import org.knot.mockejb3.interceptor.*;



/**
 * Provides the support for the container-managed transactions 
 * according to EJB spec (chapter 18).
 * <p>
 * Note that RequiredNew is not fully supported since <code>TransactionManager</code>
 * does not know how to suspend transactions. 
 * <p>
 * Transaction policy must be provided in the invocationContext in the "transactionPolicy" field.
 * If it is not provided the Supprts policy is used.
 */
public class TransactionManager implements Interceptor, Serializable {

    // logger for this class
    private static Log logger = LogFactory.getLog( TransactionManager.class.getName() );

    public final static 
        String USER_TRANSACTION_JNDI="javax.transaction.UserTransaction";

    public final static 
        String POLICY_CONTEXT_KEY="transactionPolicy";
    
    /**
     * "Cached" instance of the UserTransaction 
     */
    private static UserTransaction sharedUserTransaction;

    private TransactionPolicy policy = TransactionPolicy.SUPPORTS;

    /**
     * Creates a new instance of the <code>TransactionManager</code> with the  
     * given policy.
     * @param policy transaction policy
     */
    public TransactionManager( TransactionPolicy policy ){
        super();
        setPolicy( policy );
    }
    
    /**
     * Creates a new instance of the <code>TransactionManager</code> with the
     * default (Supports) policy.
     */
    public TransactionManager( ){
        super();
    }

    /**
     * Returns the currently set transaction policy.    
     * @return transaction policy
     */    
    public TransactionPolicy getPolicy() {
        return policy;    
    }
    
    /**
     * Sets the transaction policy.
     * @param policy policy to set.
     */
    
    public void setPolicy( TransactionPolicy policy ) {

        this.policy = policy;
    }

    /**
     * Begins, commits and rolls back the transaction according to the currently
     * set policy and EJB spec.
     */
    public void intercept( InvocationContext invocationContext ) throws Exception {
        
        
        UserTransaction newTran = null;
        
        TransactionPolicy thisCallPolicy = 
            (TransactionPolicy) invocationContext.getOptionalPropertyValue( POLICY_CONTEXT_KEY );

        if ( thisCallPolicy == null)
            thisCallPolicy = this.policy;
        Method method = invocationContext.getTargetMethod();
        if ( handlePolicy( thisCallPolicy, invocationContext.getTargetObject(), 
                method, invocationContext.getParamVals() ) ) {
        
            newTran = getUserTransaction();
            log( method, "Begin transaction" );
            newTran.begin();
                        
        }
            
        try {
            invocationContext.proceed();
            
            commitOrRollback( newTran );
            
        }

        /* Here we try to follow the guidelines of chapter 18 of EJB spec
         * with the exception of javax.transaction.TransactionRolledback
         * Note that ExceptionHandler already provides logging and wrapping services 
         * so we don't need to do it here
         */ 
        catch( Exception exception ) {
            
            if ( MockContainer.isSystemException( exception )) {   
                // If this transaction was started immediately before this invocation
                if ( newTran != null && ( newTran.getStatus()==Status.STATUS_ACTIVE ||
                    newTran.getStatus()==Status.STATUS_MARKED_ROLLBACK ) ) { 
                    newTran.rollback();
                    log( method, "Rollback because of system exception" );

                }
            }
            // if it is application exception
            // we should rollback/commit only if it was new transaction context
            else { 
                // TODO: according to the spec we must ignore the exceptions from the 
                // rollback if we try to rollback in the catch block of the business 
                // exception. We should re-throw business exception
                try {
                    commitOrRollback( newTran );
                }
                catch ( RollbackException rollbackEx ) {
                    logger.error(
                        "There has been rollback exception trying to rollback the transaction set for rollback. Ignoring. ", rollbackEx);
                }
            }
                
            throw exception;
        }
            
    }

    private void commitOrRollback( UserTransaction tran ) throws
        SystemException, RollbackException, HeuristicMixedException,
        HeuristicRollbackException {
        
        if ( tran != null ) {
        
            if ( tran.getStatus()==Status.STATUS_ACTIVE ) {
                tran.commit();
                log( "Committing transaction" );
            }
            else if ( tran.getStatus()==Status.STATUS_MARKED_ROLLBACK ) {
                tran.rollback();
                log( "Rollling back transaction" );
            }
        }        
    }
    
    /**
     * Performs the actions necessary to handle the transaction policy 
     * according to the spec. 
     * Determines whether the new transaction has to begin for the given method.
     * @param policy policy of this invoker
     * @param targetObj bean being called  
     * @param method method being called
     * @param args parameter values of the method being called
     * @return true if the new transaction must begin for the given method
     */
    // TODO: We use TransactionRequiredLocalException even for remote EJBs 
    protected boolean handlePolicy( TransactionPolicy policy, Object targetObj, 
        Method method, Object[] args) throws SystemException, NamingException  {
        
        boolean newTranRequired = false;
        
        if ( policy == TransactionPolicy.REQUIRED ){

            UserTransaction tran = getUserTransaction();
            newTranRequired=( tran == null || 
                tran.getStatus()==Status.STATUS_NO_TRANSACTION ||
                tran.getStatus()==Status.STATUS_COMMITTED ||            
                tran.getStatus()==Status.STATUS_ROLLEDBACK ||
                tran.getStatus()==Status.STATUS_UNKNOWN );
        }
        else if ( policy == TransactionPolicy.REQUIRED_NEW ){
            newTranRequired = true;        
        }
        else if ( policy == TransactionPolicy.MANDATORY ){

            UserTransaction tran = getUserTransaction();
            
            if ( tran==null ||
                tran.getStatus()==Status.STATUS_NO_TRANSACTION ||
                tran.getStatus()==Status.STATUS_COMMITTED ||            
                tran.getStatus()==Status.STATUS_ROLLEDBACK ||
                tran.getStatus()==Status.STATUS_UNKNOWN ) {
                
                throw new TransactionRequiredLocalException(
                    "Attempt to invoke method with Mandatory policy without transaction context");
            }
        }
        else if ( policy == TransactionPolicy.NEVER ){

            UserTransaction tran = getUserTransaction();
            if ( tran != null ||
                tran.getStatus()==Status.STATUS_ACTIVE ) {
                throw new EJBException(
                    "Attempt to invoke method with Never policy inside transaction context");
            }            
        }
        // For Supports we don't need to do anything
        // NotSupported is the same deal since we can't suspend the transaction            
            
        return newTranRequired;        
    }
    
    /**
     * Returns UserTransaction object. If <code>setUserTransaction()</code>
     * was called, will return the object that was set by this method.
     * Otherwise, tries to obtain UserTransaction object from JNDI. 
     * 
     * @return UserTransaction object
     */
    public static UserTransaction getUserTransaction() {
        
        UserTransaction userTransaction = null;
        
        if ( sharedUserTransaction != null ) {
            userTransaction = sharedUserTransaction;
        }
        else {
            // obtain tran from the JNDI             

            try {           

                Context context = new InitialContext();
                userTransaction = (UserTransaction) context.lookup( USER_TRANSACTION_JNDI );
            }
            catch ( NamingException namingEx ){
                throw new MockEjbSystemException(
                    "Errors while trying to obtain javax.transaction.UserTransaction from JNDI", namingEx );
            }
        }        
        
        return userTransaction;        
    }

    /**
     * Sets the shared instance of UserTransaction that will be used by MockEJB.
     * This is convenient when the remote JNDI is used and the cost of obtaining 
     * UserTransaction object from JNDI every time is too high. TransactionManager tries to 
     * get UserTransaction for every EJB method call. 
     *
     */
    public void setUserTransaction( UserTransaction userTransaction ) {
        sharedUserTransaction = userTransaction;
        
    }


    private void log( Method method, String message ){

        log( message+" for \n"+method );
    }
    
    protected void log( String message ){
        logger.debug(message);
    }
    
    /**
     * Returns true if the given object is of the same type and 
     * it has the same transaction policy.
     */
    public boolean equals( Object obj ){
        if ( ! (obj instanceof ClassPatternPointcut) )
            return false;
        
        TransactionManager transactionManager = (TransactionManager) obj;
        
        return ( this.policy == transactionManager.policy );
    }

    public int hashCode() {
        return  policy.hashCode(); 
    }    
    

}
